Design - Page Identity
October 3, 2026
|
The technote mentioned above explains why OneNote IDs are fragile and how that was measured. This document explains what OneMore does about it. This is planned future work
1. Problem and scope
OneNote regenerates every page, section, and notebook ID when a notebook is closed and reopened, and a moved page receives a new page ID. Created time, title, and content survive. OneMore stores those IDs in its database and later treats them as keys, so references silently stop resolving.
What breaks today
|
Feature |
Stored IDs |
What happens after a reopen |
|
Hashtags |
hashtag_page: pageID, sectionID, notebookID; hashtag_notebook.notebookID; a moreID stamp written into each tagged page |
The scanner matches notebooks by ID, so a reopened notebook looks new: duplicate notebook rows, and notebooks over the 100-page threshold (min for an auto-scan before prompting user for a schedule) drop out of scanning. Phantom deletion matches on section ID and tag purging on page ID, so stale rows accumulate. Identity survives only because the scanner writes a marker into each tagged page with the unintended consequence of touching the last-update-time. |
|
Favorites |
favorite: notebookID, sectionID, pageID, uri (embeds section and page GUIDs) |
Navigation uses the stored uri, which no longer resolves; the user sees "Could not navigate at this time". The "is this page already a favorite?" check compares IDs, so it misses the existing favorite and the same page can be added again. |
|
Layouts |
layout_window: notebookID, sectionID, pageID, uri |
Restore compares window.PageID with the open windows and navigates by uri. Both fail and the window is silently skipped. |
There is a manual repair path, TargetChecker (the Check button in the manage dialogs). It resolves by ID and then by location path, but it is manual, matches by title path only, and its repairs are never saved: UpdateFavorite and UpdateWindow do not write IDs or the URI, so a repaired reference reverts when the dialog closes.
Goals
- A reference to a page, section, section group, or notebook keeps resolving across close/reopen, page moves (between sections and between notebooks), and creation-date edits.
- Nothing is written to the user's pages to identify them - hence does not touch the last-update-time.
- One shared mechanism serves hashtags, favorites, and layouts, rather than three private ones.
- References that cannot be resolved are shown as broken, never silently dropped or deleted.
Non-goals
- The full-text search index (planned later; it will consume this layer).
- Bookmarks and reminders. Both store OneNote IDs and are natural future consumers, but are out of scope here.
- Roaming or sharing OneMore.db between machines. Each machine keeps its own database and its own keys.
- Paragraph (object) IDs. Their stability across a reopen has not been measured.
2. Concepts
Page key
A page key is an integer assigned by OneMore in its own database (identity_page.pageKey, AUTOINCREMENT). It is not derived from the page, so OneNote cannot change it. What can fail is attaching the key to the right page again after OneNote changes the IDs; that is the job of the matcher.
Keys are never reused after a record is deleted. A reused key would attach data stored against the old page to a different page.
The page key is a signed 64-bit integeger, so has a range of 1 to 263-1
Container keys
Notebook and section IDs also change, so containers are identified by name and path instead of a surrogate:
|
Key |
Built from |
|
|
the notebook's path (cloud URL or local folder), trimmed and lower-cased; the name if there is no path |
|
|
the names of the enclosing section groups, outermost first, plus the section name, e.g. /Group A/More Group/Section 3 |
Fingerprint
What is known about a page from the hierarchy alone, without loading it: notebookKey, sectionKey, title, created (millisecond precision), and modified. modified is used only to pair pages that are otherwise identical.
Resolution ladder
Each current page that has no stored record with the same OneNote ID is compared against the stored records that no page has claimed, strongest evidence first. Each step considers only what earlier steps left unclaimed.
|
Step |
Evidence |
Covers |
Cost |
|
1 |
Same OneNote page ID |
nothing changed |
none |
|
2 |
notebook + section + title + created |
notebook reopen |
hierarchy only |
|
3 |
notebook + title + created |
page moved to another section |
hierarchy only |
|
4 |
title + created |
page moved to another notebook |
hierarchy only |
|
5 |
notebook + section + title |
creation date edited |
hierarchy only; flagged as a weak match |
|
6 |
none |
a new page |
none |
Measured results for steps 2 to 5 are in the technote.
A further step that loads the page and matches a hash of its text was considered and deliberately left out of this design. Nothing in hashtags, favorites, or layouts needs it; only the later text index would. It will be added with the index, as a small upgrade of the identity catalog (section 7). When a page matches nothing above, it is treated as a new page, which costs a re-read and is never wrong.
Rules
- One-to-one only. A step matches a page to a record only when exactly one candidate qualifies on each side. If several pages are indistinguishable, the match is declined and they are treated as new. That costs a re-read, but is never wrong. The single exception is identical pages in the same location with the same modified time: their content is the same, so which record each gets does not matter.
- Claimed records are excluded. A page copied into another notebook keeps its title and creation time. Because the original claims its own record by ID first, the copy cannot be mistaken for it.
- A weak match (step 5) forces a re-read of the page by any consumer that caches content, because title alone is not proof.
- Do not delete on the first miss. A page that is not seen is marked missing (missingSince) and kept. It is deleted only after a grace period (hours), because a reopened notebook fills in over minutes. If it reappears under a new ID, the ladder reconnects it.
- Skipped sections are not deleted sections. A locked or unreadable section is recorded as skipped for that pass, so its pages are left alone instead of being marked missing.
- Refresh created on every pass. A creation-date edit changes neither the page ID nor lastModifiedTime, so the stored value must be refreshed from the hierarchy every time.
What is deliberately not part of identity
- No marker written into pages. The earlier omPageID stamp worked across reopens but is a write to a page the user did not edit.
- No reliance on OneNote IDs beyond one session. They are used to load or navigate now, never remembered.
3. Architecture
|
Architecture PlantUML (Extract) |
Components
|
Component |
Responsibility |
|
IdentityService |
Runs a hierarchy-only pass over all open notebooks, independent of hashtag settings: one hierarchy call per notebook, no page loads. Builds the current page list, calls the matcher, writes results, purges long-missing pages. |
|
PageIdentityMatcher |
Pure function: stored records + current pages in, resolutions out. No database, no OneNote. Easy to test exhaustively. |
|
Identity catalog (identity_page) |
The page keys and their last-seen fingerprints. |
|
Identity resolver |
Small read API for consumers: key -> current page ID, section ID, notebook ID, URI, page ID -> key, and register this page at creation time. |
|
Hashtag stage |
The existing scan, now consuming identity instead of keeping its own. |
|
Workspace healer |
After each pass, repairs favorites and layout windows and persists the repair. |
Why a separate, always-on identity pass
Favorites and layouts can point at pages in any notebook, including ones the user excluded from hashtag scanning, or when hashtag scanning is disabled. If identity were produced only by the hashtag scan, those references would have nothing to resolve against. The pass is cheap because it loads no page content.
Pipeline order for each cycle: identity pass, then hashtag stage, then workspace healing.
Decision: one pipeline in one background loop. The three stages run in that order every cycle, so a hashtag scan can never run against identity that has not caught up. The hashtag service's disabled setting skips only the hashtag stage; the identity pass and healing keep running, because favorites and layouts depend on them. The tray's scheduled scan and rebuild call the same pipeline, so they too begin with an identity pass. The pipeline's tuning values, such as the missing-page grace period, are named constants.
4. Data model
|
Data Model PlantUML (Extract) |
Notes:
- The OneNote IDs that remain in favorite and layout_window are last-known values, kept current by the healer. They are a cache, not the identity.
- layout_window.pageID stays NOT NULL, so a newly captured window always stores the ID it had when saved.
- Notebook, section-group, and section favorites carry no pageKey. They store notebookKey and sectionKey and are re-resolved by name and path.
- The hashtag tables keep the moreID column name and simply hold the page key as text. Renaming it would require a table rebuild for no behavioral gain.
- There is deliberately no contentHash column. The later text index will add one (with the matching step) as an identity 1 to 2 upgrade.
5. Behavior
5.1 Background identity pass
|
Background Identity Pass PlantUML (Extract) |
A notebook whose hierarchy could not be read is left out of the scope of the pass, so a failed read is never mistaken for an empty notebook.
5.2 A notebook is reopened
|
Notebook Reopened PlantUML (Extract) |
5.3 Navigating a favorite
|
Navigating Favorite PlantUML (Extract) |
5.4 Restoring a layout
|
Restoring a Layout PlantUML (Extract) |
5.5 Adding a favorite or saving a layout
The page's fingerprint is captured at creation time, so the reference is born with a key.
|
Adding a Favorite PlantUML (Extract) |
5.6 Background healing
|
Background Healing PlantUML (Extract) |
6. Applying it to each feature
6.1 Hashtags
- Tags are keyed by the page key, stored as text in the existing moreID columns. ReadPageTags and WriteTags use the key; hashtag_page is joined by key, not by pageID.
- The scanner stops matching by ID and stops writing omPageID into pages (the stamp set by HashtagPageScanner.SetMoreID and written by HashtagScanner.ScanPage, and the stamp in DuplicatePageCommand). It keeps its one legitimate page write, which applies the hashtag style the user chose.
- Notebook rows are adopted by name after a reopen: a stored notebook whose ID is gone is taken to be the open notebook of the same name, keeping its inclusion setting and last scan. If either duplicate was excluded, the notebook stays excluded. This also removes the 100-page "new notebook" threshold problem for reopened notebooks.
- Which pages are read: changed pages, new pages, weak matches, and pages that have tags and whose ID changed (the object IDs stored with their tags are probably stale). Reopening a notebook therefore re-reads only its tagged pages, not the whole notebook.
- Stored page info (pageID, sectionID, notebookID, path, name) is refreshed from the hierarchy for every tagged page on every pass, because the dialogs compare these with the IDs of the notebooks open now.
- Deleting tags is tied to purging missing pages after the grace period, replacing per-section phantom deletion.
- One-time migration: tags recorded under the old stamp keys are cleared and the scan time reset, so one background scan rebuilds them. Tags are derived from page text, so nothing is lost; notebook selections are kept.
- HashtagCommand finds the current page's key by asking the resolver for its page ID, instead of reading a stamp from the page.
6.2 Favorites
- Page favorites gain pageKey. Navigation resolves the key to current IDs and a freshly generated URI (section 5.3).
- Section, section-group, and notebook favorites gain notebookKey and sectionKey and are re-resolved by name and path, using the same keys the identity layer already builds. They need no surrogate and no scan, only a resolve step. A notebook favorite today stores the notebook ID in sectionID and uri; the key columns replace that dependence.
- Unique indexes. idx_favorite_target_page (on pageID) and idx_favorite_target_section (on sectionID) are partial unique indexes and cannot be altered, so they are dropped and recreated on the keys inside the upgrade transaction.
- Legacy rows (created before keys existed) are backfilled lazily by the healer, using TargetChecker's existing logic: match the stored IDs against the current hierarchy, else walk location by name. Healed values are then persisted, which today they are not.
- The existing root-folder convention (folderID = 0, not NULL) is left alone.
- The manual Check remains as a fallback and now saves what it repairs.
6.3 Layouts
- layout_window gains pageKey.
- RestoreLayoutCommand compares open windows against the resolved page ID, and navigates by the resolved URI.
- A window that cannot be resolved is reported, not silently skipped.
- LayoutsProvider has no version table today. It gains layouts_schema, following exactly what favorites did when it introduced favorites_schema.
6.4 Export and import compatibility
Favorites and layouts can be exported to JSON and imported again, possibly after the database has been upgraded, or on another machine. The existing code is already tolerant, which is what this design relies on:
- Export serializes the model objects with Newtonsoft (ExportFavoritesCommand, ExportLayoutsCommand). Import deserializes with default settings, so properties missing from the file take their defaults and unknown properties are ignored. It then writes each row through WriteFavorite / WriteWindow, which use an explicit column list rather than the old schema's shape. Importing resets every database-local ID first (favorite.ID = 0, FolderID and LayoutID reassigned).
What the design must therefore guarantee:
- An old file imports into an upgraded database. Old files have no pageKey, notebookKey, or sectionKey, so those arrive unset. The new WriteFavorite / WriteWindow must accept that and store NULL. The model property for pageKey must be nullable: a plain integer would deserialize as 0 and be stored as a real key.
- pageKey is database-local, like favorite.ID, and is never trusted from a file. A key is only meaningful in the database that issued it. Key 17 on another machine, or in a database that was since reset, is a different page. Export therefore omits it, and import discards any value it finds, exactly as it already discards ID. Imported rows are resolved afterwards by the healer.
- Imported rows are treated as legacy rows. The healer backfills them the same way as rows that predate the upgrade: stored IDs first (valid only if they happen to match this machine's current IDs), then location path plus title. A file from another machine will mostly resolve by the location fallback, with today's limits: no created time, so duplicate titles can be ambiguous, and unresolved rows are shown as broken.
- Duplicate detection must not depend on pageKey alone. The unique indexes move to the keys, but imported rows have none yet, so a re-import could create duplicates that the old pageID index used to reject. The import path needs its own duplicate check (on location and title, or on the stored IDs) until the row is resolved.
- The healer must handle two rows resolving to the same page. After backfill, two legacy rows can map to one key, which the new unique index forbids. The healer keeps the first and reports the other as a duplicate instead of failing.
- A newer file imports into an older build. Because unknown properties are ignored, the added fields are simply dropped. This holds only while no import code turns on strict member handling, so it is called out as a constraint.
- Enrichment (decided). Export adds title, created, notebookKey, and sectionKey to each favorite and layout window, plus a small formatVersion field at the top of the file. An import on another machine can then resolve against the local hierarchy by title and creation time instead of falling back to location and title alone. It is additive and compatible with every point above: old builds ignore the new fields, and the formatVersion lets later changes detect which shape they are reading. pageKey is still never exported.
7. Migration and versioning (summary)
Each catalog keeps its own version table and upgrade chain. The implementation plan will give the details; this is the shape.
|
Catalog |
Version table |
Now |
Target |
Schema change |
Data step |
|
Page identity |
|
none |
1 |
new tables identity_page and indexes |
none |
|
Hashtags |
|
5 (in main) |
6 |
none (the moreID column now holds the page key) |
clear tags, reset scan time (rebuild once) |
|
Favorites |
|
2 |
3 |
add pageKey, notebookKey, sectionKey; recreate the two unique indexes |
lazy, by the healer |
|
Layouts |
none (implicit 1) |
1 |
2 |
create layouts_schema; add pageKey; recreate the unique index |
lazy, by the healer |
Principles:
- Schema changes are pure SQL inside UpgradeCatalog, one transaction per step, ending with the version update, so a failed step leaves the old version in place. Providers are constructed in many contexts (dialogs, the tray, the calendar app) and must never need OneNote to open.
- Data that needs OneNote is deferred. Resolving legacy favorites and layout windows needs the hierarchy, so it is done by the healer when OneNote is available, not during the upgrade. Until then, rows behave exactly as they do today.
- Fresh databases get the final shape from the embedded DDL at the current version; existing databases get the upgrade chain. Both paths must produce the same schema and are tested against each other.
- A database newer than the code is left alone. If a catalog reports a version the code does not know, the provider logs it and runs no step, rather than guessing. (A developer database can hold a newer version from another build.)
- Shared helpers. ColumnExists and an upsert-style UpgradeSchemaVersion are currently private to individual providers and are lifted into DatabaseProvider. The upsert form is required because a version row may not exist yet (as with layouts_schema).
- DDL stays single-line per statement with the CREATE ... IF NOT EXISTS name form, because RefreshDataSchema and DropCatalog parse it that way.
8. Failure modes and open questions
Known limits
|
Case |
Outcome |
|
Page renamed, moved, and creation date edited all at once, then reopened |
No hierarchy signal remains. It becomes a new page with a new key; its old key is purged after the grace period. Hashtags are rebuilt from content; a favorite shows as broken. |
|
Several pages identical on every signal |
Paired only if their location and modified time match (content is the same); otherwise treated as new. |
|
Reopened notebook not fully loaded at the next pass |
Pages already listed are rehomed; the rest are marked missing and kept. |
|
Stored page ID between a reopen and the next pass |
Stale. Consumers resolve through the identity row and fall back to a targeted single-notebook reconcile (section 5.3). |
|
Locked or encrypted section |
Its pages are skipped, not deleted. They cannot be hashed or read while locked. |
|
Closed notebook |
Its records are left untouched, and re-match by fingerprint when it is reopened. |
Decisions
|
# |
Question |
Decision |
|
1 |
Service topology |
One pipeline, one loop: identity, then hashtags, then healing. The hashtag disabled setting skips only its own stage. The tray's scan and rebuild run the same pipeline. |
|
2 |
Grace period for a missing page |
6 hours, as a named constant. Longer than any measured reload, short enough that deleted pages leave hashtag results the same day. Not a user setting. |
|
3 |
Export format |
Enriched: title, created, notebookKey, sectionKey, plus a formatVersion field. pageKey is never exported. |
|
4 |
Content-hash matching and omPageID stamps |
Left out until the text index. No contentHash column and no page-loading step in identity v1. Old omPageID stamps are not read. |
Open questions
These are measurements and a deferral, not preferences. They need OneNote to answer, or the work that creates the need.
- Cadence and cost on large notebooks. The largest notebook measured had 447 live pages. The old scanner capped new notebooks at 100 pages, apparently to protect OneNote's responsiveness. The hierarchy-only pass needs to be timed on a much larger notebook, and a cheap skip (for example, comparing a notebook's lastModifiedTime) should be evaluated. This sets the default cycle length.
- Does the pages-scope hierarchy include page-level metadata? The hashtag scanner skips its own tag-index pages by reading a page meta element obtained through GetSection. If the notebook-wide pages scope does not carry it, either section calls remain or another way to skip those pages is needed.
- Paragraph identity is unmeasured. Anything that links to a specific paragraph needs its own test before it can build on this layer. This matters most for the later text index, whose results navigate to a paragraph by its object ID.
Not measured
Paragraph object IDs across a reopen, a second machine, renames of notebooks, sections, or pages, moving or renaming section groups, locked sections, the OneNote UI as the route for moves and date edits (the experiments used the API), other platforms, and very large notebooks. See the technote.
9. Testing strategy
|
Layer |
Approach |
|
Matcher |
Pure unit tests for every ladder step and every rule: reopen, gradual reopen, section move, notebook move, edited date, identical duplicates, a copy beside its original, strong signal preferred over weak, ambiguity declined. |
|
Identity provider |
In-memory SQLite (the internal constructor taking a SQLiteConnection, as in FavoritesProvider): persistence, rehoming, soft-missing and purge, key never reused, skipped sections, notebooks out of scope left alone. |
|
Upgrades |
For each catalog, build a database at the old version from the old DDL, open the new provider, and assert the schema, data, version, and that it matches a fresh database. Include a database newer than the code. |
|
Consumers |
Provider-level tests for the hashtag methods, favorites and layouts resolution, healing that persists, and unresolved references reported. The decision of which pages to read is a small, directly tested function. |
|
Import and Export |
Keep fixture JSON files in the old format (captured from the current export commands before they change). Test that each imports into a database upgraded from the old version: rows are stored with `NULL` keys, a stray pageKey in a file is discarded, duplicates are still rejected, and two rows resolving to one page are handled. Also test that a file with extra, unknown properties still imports. |
|
Manual (needs OneNote) |
Close and reopen each notebook type and confirm favorites, layouts, and hashtags still resolve; move a page between sections and notebooks; edit a creation date and reopen; a half-loaded notebook; a locked section. Scripts from the measurement work can capture before and after snapshots. |
The scan loop and the commands talk to OneNote directly and cannot be unit tested; the design keeps logic out of them so that what is left untested is thin.
═══════════════════════════════════════════════════════════════════════════════════════════════════
Architecture PlantUML (Refresh)
@startuml
skinparam componentStyle rectangle
skinparam shadowing false
package "OneNote" {
component "Hierarchy" as H
}
package "OneMore add-in" {
component "IdentityService" as IS
component "PageIdentityMatcher" as M
component "Identity resolver" as R
component "Hashtag stage" as HS
component "Workspace healer" as WH
component "FavoritesCommand,\nFavoritesMenu" as FC
component "RestoreLayoutCommand" as RL
component "SaveLayoutCommand,\nAddFavoriteCommand" as SV
}
package "OneMore.db" {
database "identity_page" as IDP
database "hashtag tables" as HT
database "favorite" as FAV
database "layout tables" as LAY
}
H --> IS : pages + attributes
IS --> M : PageRefs vs stored rows
M --> IS : resolutions
IS --> IDP : reconcile, soft-missing, purge
IS ..> HS : after each pass
IS ..> WH : after each pass
HS --> HT
WH --> FAV
WH --> LAY
FC --> R
RL --> R
SV --> R : register page at creation
R --> IDP
FC --> FAV
RL --> LAY
@enduml
Data Model PlantUML (Refresh)
@startuml
hide circle
skinparam linetype ortho
entity identity_page {
* pageKey : INTEGER <<PK, AUTOINCREMENT>>
--
pageID : TEXT (last seen, session-scoped)
notebookKey : TEXT
sectionKey : TEXT
title : TEXT
created : TEXT
modified : TEXT
level : INTEGER
missingSince : TEXT (NULL = present)
lastSeen : TEXT
}
entity hashtag_page {
* moreID : TEXT (holds the page key)
--
pageID : TEXT (refreshed each pass)
notebookID : TEXT (refreshed)
sectionID : TEXT (refreshed)
path, name, titleID
}
entity hashtag {
* tag, objectID
--
moreID : TEXT (page key)
}
entity favorite {
* favoriteID : INTEGER
--
pageKey : INTEGER (new, NULL for containers)
notebookKey : TEXT (new)
sectionKey : TEXT (new; group or section path)
kind : TEXT
notebookID, sectionID, pageID, uri (last known)
name, alias, location, folderID, sortOrder
}
entity layout_window {
* windowID : INTEGER
--
pageKey : INTEGER (new)
notebookID, sectionID, pageID, uri (last known)
name, alias, location, zOrder, device, bounds
}
identity_page ||--o{ hashtag_page : moreID = pageKey
hashtag_page ||--o{ hashtag : moreID
identity_page ||--o{ favorite : pageKey
identity_page ||--o{ layout_window : pageKey
@enduml
Background Identity Pass PlantUML (Refresh)
@startuml
participant "IdentityService" as S
participant "OneNote" as O
participant "Matcher" as M
database "identity_page" as DB
S -> O : GetNotebooks()
loop each open notebook
S -> O : GetNotebook(id, pages)
O --> S : sections, pages (ID, name, created, modified)
S -> S : build PageRef list;\nnote locked sections as skipped
end
S -> DB : read candidate rows\n(scoped notebooks + missing rows)
S -> M : Match(rows, pages)
M --> S : resolutions + orphans
S -> DB : insert new, update changed,\nmark orphans missing
S -> DB : purge missing longer than grace
S -> S : notify hashtag stage and healer
@enduml
Notebook Reopened PlantUML (Refresh)
@startuml
actor User
participant "IdentityService" as S
database "identity_page" as DB
User -> User : close and reopen notebook\n(all IDs regenerated)
S -> DB : pass 1: only part of the notebook is listed
note right of DB
Listed pages are rehomed by fingerprint:
same key, new page ID.
Pages not yet listed are marked missing,
NOT deleted.
end note
S -> DB : pass 2: more pages listed, more rehomed
S -> DB : pass N: whole notebook listed
note right of DB
Every page has its original key.
missingSince is cleared.
Nothing was lost.
end note
@enduml
Navigating Favorite PlantUML (Refresh)
@startuml
actor User
participant "FavoritesCommand" as C
participant "Resolver" as R
database "identity_page" as DB
participant "OneNote" as O
database "favorite" as F
User -> C : click favorite
C -> R : Resolve(favorite.pageKey)
R -> DB : read row
DB --> R : current pageID, section, notebook
R -> O : GetHyperlink(pageID)
O --> R : uri
alt resolved and navigation succeeds
C -> O : NavigateTo(uri)
C -> F : persist refreshed IDs and uri\n(if they changed)
else stale or unresolved
R -> O : read just that notebook's hierarchy
R -> R : reconcile that notebook, retry
alt now resolved
C -> O : NavigateTo(uri)
C -> F : persist refreshed IDs and uri
else still unresolved
C -> User : mark favorite as broken\n(never deleted)
end
end
@enduml
Restoring a Layout PlantUML (Refresh)
@startuml
participant "RestoreLayoutCommand" as C
participant "Resolver" as R
participant "OneNote" as O
loop each layout_window
C -> R : Resolve(window.pageKey)
R --> C : current pageID + uri (or unresolved)
alt resolved
C -> O : find open window for resolved pageID
alt not already open
C -> O : NavigateTo(resolved uri, newWindow)
C -> O : wait for window with resolved pageID
end
C -> O : position window
else unresolved
C -> C : record as skipped and report it\n(instead of silently ignoring)
end
end
@enduml
Adding a Favorite PlantUML (Refresh)
@startuml
participant "AddFavoriteCommand /\nSaveLayoutCommand" as C
participant "OneNote" as O
participant "Resolver" as R
database "identity_page" as DB
C -> O : read current page\n(title, created, section path, notebook path)
C -> R : EnsurePage(pageRef)
R -> DB : find by ID, else reconcile, else insert
R --> C : pageKey
C -> C : store pageKey + last-known IDs and uri
@enduml
Background Healing PlantUML (Refresh)
@startuml
participant "Workspace healer" as W
database "identity_page" as ID
database "favorite / layout_window" as T
participant "OneNote" as O
W -> T : rows needing attention
note right of T
1. pageKey is NULL (legacy rows)
2. stored page ID differs from
the identity's current ID
end note
alt legacy row (no pageKey)
W -> ID : match stored IDs against current rows
alt no match
W -> O : resolve by location path + title
end
W -> T : set pageKey, refresh IDs, location
else known key, stale IDs
W -> ID : current IDs
end
W -> O : GetHyperlink(page) to regenerate uri
W -> T : persist IDs and uri
@enduml
#omwiki #omdeveloper #omdesign
© 2026 Steven M Cohn. All rights reserved.
Please consider a sponsorship or one-time donation to support ongoing development
Created with OneNote.







